//! Management of the directory layout of a build
//!
//! The directory layout is a little tricky at times, hence a separate file to
//! house this logic. The current layout looks like this:
//!
//! ```text
//! # This is the root directory for all output, the top-level package
//! # places all of its output here.
//! target/
//!
//!     # Cache of `rustc -Vv` output for performance.
//!     .rustc-info.json
//!
//!     # All final artifacts are linked into this directory from `deps`.
//!     # Note that named profiles will soon be included as separate directories
//!     # here. They have a restricted format, similar to Rust identifiers, so
//!     # Cargo-specific directories added in the future should use some prefix
//!     # like `.` to avoid name collisions.
//!     debug/  # or release/
//!
//!         # File used to lock the directory to prevent multiple cargo processes
//!         # from using it at the same time.
//!         .cargo-lock
//!
//!         # Hidden directory that holds all of the fingerprint files for all
//!         # packages
//!         .fingerprint/
//!             # Each package is in a separate directory.
//!             # Note that different target kinds have different filename prefixes.
//!             $pkgname-$META/
//!                 # Set of source filenames for this package.
//!                 dep-lib-$targetname
//!                 # Timestamp when this package was last built.
//!                 invoked.timestamp
//!                 # The fingerprint hash.
//!                 lib-$targetname
//!                 # Detailed information used for logging the reason why
//!                 # something is being recompiled.
//!                 lib-$targetname.json
//!                 # The console output from the compiler. This is cached
//!                 # so that warnings can be redisplayed for "fresh" units.
//!                 output-lib-$targetname
//!
//!         # This is the root directory for all rustc artifacts except build
//!         # scripts, examples, and test and bench executables. Almost every
//!         # artifact should have a metadata hash added to its filename to
//!         # prevent collisions. One notable exception is dynamic libraries.
//!         deps/
//!
//!             # Each artifact dependency gets in its own directory.
//!             /artifact/$pkgname-$META/$kind
//!
//!         # Root directory for all compiled examples.
//!         examples/
//!
//!         # Directory used to store incremental data for the compiler (when
//!         # incremental is enabled.
//!         incremental/
//!
//!         # This is the location at which the output of all custom build
//!         # commands are rooted.
//!         build/
//!
//!             # Each package gets its own directory where its build script and
//!             # script output are placed
//!             $pkgname-$META/    # For the build script itself.
//!                 # The build script executable (name may be changed by user).
//!                 build-script-build-$META
//!                 # Hard link to build-script-build-$META.
//!                 build-script-build
//!                 # Dependency information generated by rustc.
//!                 build-script-build-$META.d
//!                 # Debug information, depending on platform and profile
//!                 # settings.
//!                 <debug symbols>
//!
//!             # The package shows up twice with two different metadata hashes.
//!             $pkgname-$META/  # For the output of the build script.
//!                 # Timestamp when the build script was last executed.
//!                 invoked.timestamp
//!                 # Directory where script can output files ($OUT_DIR).
//!                 out/
//!                 # Output from the build script.
//!                 output
//!                 # Path to `out`, used to help when the target directory is
//!                 # moved.
//!                 root-output
//!                 # Stderr output from the build script.
//!                 stderr
//!
//!     # Output from rustdoc
//!     doc/
//!
//!     # Used by `cargo package` and `cargo publish` to build a `.crate` file.
//!     package/
//!
//!     # Experimental feature for generated build scripts.
//!     .metabuild/
//! ```
//!
//! When cross-compiling, the layout is the same, except it appears in
//! `target/$TRIPLE`.

use crate::core::Workspace;
use crate::core::compiler::CompileTarget;
use crate::util::flock::is_on_nfs_mount;
use crate::util::{CargoResult, FileLock};
use cargo_util::paths;
use std::path::{Path, PathBuf};

/// Contains the paths of all target output locations.
///
/// See module docs for more information.
pub struct Layout {
    artifact_dir: Option<ArtifactDirLayout>,
    build_dir: BuildDirLayout,
}

impl Layout {
    /// Calculate the paths for build output, lock the build directory, and return as a Layout.
    ///
    /// This function will block if the directory is already locked.
    ///
    /// `dest` should be the final artifact directory name. Currently either
    /// "debug" or "release".
    pub fn new(
        ws: &Workspace<'_>,
        target: Option<CompileTarget>,
        dest: &str,
    ) -> CargoResult<Layout> {
        let is_new_layout = ws.gctx().cli_unstable().build_dir_new_layout;
        let mut root = ws.target_dir();
        let mut build_root = ws.build_dir();
        if let Some(target) = target {
            root.push(target.short_name());
            build_root.push(target.short_name());
        }
        let build_dest = build_root.join(dest);
        let dest = root.join(dest);
        // If the root directory doesn't already exist go ahead and create it
        // here. Use this opportunity to exclude it from backups as well if the
        // system supports it since this is a freshly created folder.
        //
        paths::create_dir_all_excluded_from_backups_atomic(root.as_path_unlocked())?;
        if root != build_root {
            paths::create_dir_all_excluded_from_backups_atomic(build_root.as_path_unlocked())?;
        }

        // Now that the excluded from backups target root is created we can create the
        // actual destination (sub)subdirectory.
        paths::create_dir_all(dest.as_path_unlocked())?;

        // For now we don't do any more finer-grained locking on the artifact
        // directory, so just lock the entire thing for the duration of this
        // compile.
        let artifact_dir_lock = if is_on_nfs_mount(root.as_path_unlocked()) {
            None
        } else {
            Some(dest.open_rw_exclusive_create(".cargo-lock", ws.gctx(), "artifact directory")?)
        };

        let build_dir_lock = if root == build_root || is_on_nfs_mount(build_root.as_path_unlocked())
        {
            None
        } else {
            Some(build_dest.open_rw_exclusive_create(
                ".cargo-lock",
                ws.gctx(),
                "build directory",
            )?)
        };
        let root = root.into_path_unlocked();
        let build_root = build_root.into_path_unlocked();
        let dest = dest.into_path_unlocked();
        let build_dest = build_dest.as_path_unlocked();
        let deps = build_dest.join("deps");
        let artifact = deps.join("artifact");

        Ok(Layout {
            artifact_dir: Some(ArtifactDirLayout {
                dest: dest.clone(),
                examples: dest.join("examples"),
                doc: root.join("doc"),
                timings: root.join("cargo-timings"),
                _lock: artifact_dir_lock,
            }),
            build_dir: BuildDirLayout {
                root: build_root.clone(),
                deps,
                build: build_dest.join("build"),
                artifact,
                incremental: build_dest.join("incremental"),
                fingerprint: build_dest.join(".fingerprint"),
                examples: build_dest.join("examples"),
                tmp: build_root.join("tmp"),
                _lock: build_dir_lock,
                is_new_layout,
            },
        })
    }

    /// Makes sure all directories stored in the Layout exist on the filesystem.
    pub fn prepare(&mut self) -> CargoResult<()> {
        if let Some(ref mut artifact_dir) = self.artifact_dir {
            artifact_dir.prepare()?;
        }
        self.build_dir.prepare()?;

        Ok(())
    }

    pub fn artifact_dir(&self) -> Option<&ArtifactDirLayout> {
        self.artifact_dir.as_ref()
    }

    pub fn build_dir(&self) -> &BuildDirLayout {
        &self.build_dir
    }
}

pub struct ArtifactDirLayout {
    /// The final artifact destination: `<artifact-dir>/debug` (or `release`).
    dest: PathBuf,
    /// The directory for examples
    examples: PathBuf,
    /// The directory for rustdoc output
    doc: PathBuf,
    /// The directory for --timings output
    timings: PathBuf,
    /// The lockfile for a build (`.cargo-lock`). Will be unlocked when this
    /// struct is `drop`ped.
    _lock: Option<FileLock>,
}

impl ArtifactDirLayout {
    /// Makes sure all directories stored in the Layout exist on the filesystem.
    pub fn prepare(&mut self) -> CargoResult<()> {
        paths::create_dir_all(&self.examples)?;

        Ok(())
    }
    /// Fetch the destination path for final artifacts  (`/…/target/debug`).
    pub fn dest(&self) -> &Path {
        &self.dest
    }
    /// Fetch the examples path.
    pub fn examples(&self) -> &Path {
        &self.examples
    }
    /// Fetch the doc path.
    pub fn doc(&self) -> &Path {
        &self.doc
    }
    /// Fetch the cargo-timings path.
    pub fn timings(&self) -> &Path {
        &self.timings
    }
}

pub struct BuildDirLayout {
    /// The root directory: `/path/to/build-dir`.
    /// If cross compiling: `/path/to/build-dir/$TRIPLE`.
    root: PathBuf,
    /// The directory with rustc artifacts
    deps: PathBuf,
    /// The primary directory for build files
    build: PathBuf,
    /// The directory for artifacts, i.e. binaries, cdylibs, staticlibs
    artifact: PathBuf,
    /// The directory for incremental files
    incremental: PathBuf,
    /// The directory for fingerprints
    fingerprint: PathBuf,
    /// The directory for pre-uplifted examples: `build-dir/debug/examples`
    examples: PathBuf,
    /// The directory for temporary data of integration tests and benches
    tmp: PathBuf,
    /// The lockfile for a build (`.cargo-lock`). Will be unlocked when this
    /// struct is `drop`ped.
    ///
    /// Will be `None` when the build-dir and target-dir are the same path as we cannot
    /// lock the same path twice.
    _lock: Option<FileLock>,
    is_new_layout: bool,
}

impl BuildDirLayout {
    /// Makes sure all directories stored in the Layout exist on the filesystem.
    pub fn prepare(&mut self) -> CargoResult<()> {
        if !self.is_new_layout {
            paths::create_dir_all(&self.deps)?;
            paths::create_dir_all(&self.fingerprint)?;
        }
        paths::create_dir_all(&self.incremental)?;
        paths::create_dir_all(&self.examples)?;
        paths::create_dir_all(&self.build)?;

        Ok(())
    }
    /// Fetch the deps path.
    pub fn deps(&self, pkg_dir: &str) -> PathBuf {
        if self.is_new_layout {
            self.build_unit(pkg_dir).join("deps")
        } else {
            self.legacy_deps().to_path_buf()
        }
    }
    /// Fetch the deps path. (old layout)
    pub fn legacy_deps(&self) -> &Path {
        &self.deps
    }
    pub fn root(&self) -> &Path {
        &self.root
    }
    /// Fetch the build examples path.
    pub fn examples(&self) -> &Path {
        &self.examples
    }
    /// Fetch the incremental path.
    pub fn incremental(&self) -> &Path {
        &self.incremental
    }
    /// Fetch the fingerprint path.
    pub fn fingerprint(&self, pkg_dir: &str) -> PathBuf {
        if self.is_new_layout {
            self.build_unit(pkg_dir).join("fingerprint")
        } else {
            self.legacy_fingerprint().to_path_buf().join(pkg_dir)
        }
    }
    /// Fetch the fingerprint path. (old layout)
    pub fn legacy_fingerprint(&self) -> &Path {
        &self.fingerprint
    }
    /// Fetch the build path.
    pub fn build(&self) -> &Path {
        &self.build
    }
    /// Fetch the build script path.
    pub fn build_script(&self, pkg_dir: &str) -> PathBuf {
        if self.is_new_layout {
            self.build_unit(pkg_dir).join("build-script")
        } else {
            self.build().join(pkg_dir)
        }
    }
    /// Fetch the build script execution path.
    pub fn build_script_execution(&self, pkg_dir: &str) -> PathBuf {
        if self.is_new_layout {
            self.build_unit(pkg_dir).join("build-script-execution")
        } else {
            self.build().join(pkg_dir)
        }
    }
    /// Fetch the artifact path.
    pub fn artifact(&self) -> &Path {
        &self.artifact
    }
    /// Fetch the build unit path
    pub fn build_unit(&self, pkg_dir: &str) -> PathBuf {
        self.build().join(pkg_dir)
    }
    /// Create and return the tmp path.
    pub fn prepare_tmp(&self) -> CargoResult<&Path> {
        paths::create_dir_all(&self.tmp)?;
        Ok(&self.tmp)
    }
}
